fix(identity): close generic-write apiMethods hole on sys_presence & sys_metadata (#3220)#3222
Merged
Merged
Conversation
…sys_metadata (#3220) Follow-through on #1591/#3213 for two non-better-auth managed objects that shipped the same contradiction the better-auth reconciliation fixed: their enable.apiMethods advertised generic create/update/delete while their managedBy bucket forbids user-context writes, leaving the generic /data route open. - sys_presence (append-only) advertised create/update/delete — update/delete on an append-only object — but is written only over the realtime websocket/ in-memory path, never through ObjectQL. Narrowed to ['get','list']. - sys_metadata (system) advertised full CRUD but overlays are authored only via the metadata-protocol RPC (engine writes carry a transaction context, not a user session); neither the framework nor the Console (objectui) POSTs /data/sys_metadata. Narrowed to ['get','list']. Reads stay open. The metadata-protocol / realtime write paths are engine-level and bypass the HTTP exposure gate, so they are unaffected — verified by the metadata-authoring dogfood (5 passed), the objectql overlay engine-insert tests (6 passed), and metadata-core (100 passed). A blast-radius audit found the broader system/append-only buckets are NOT safe to guard wholesale: several system objects (sys_user_position, sys_user_permission_set, sys_position_permission_set, sys_user_preference, sys_import_job) are user-writable by design (delegated administration, user preferences, imports). Generalizing the engine write guard to those buckets is intentionally out of scope — the root cause is the overloaded system bucket, tracked in #3220. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwY68hKVysP98BX7i5eUoE
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
3 tasks
os-zhuang
marked this pull request as ready for review
July 18, 2026 15:00
os-zhuang
added a commit
that referenced
this pull request
Jul 19, 2026
…ritable, guard engine-owned writes (#3220) (#3315) The `system` bucket conflated two incompatible write policies — engine-owned rows (never user-written) and platform-schema, admin/user-writable data — under one all-false affordance row with no engine enforcement (only better-auth had a write guard). This left a wildcard admin able to raw-write engine-owned rows through the generic data API (ADR-0049 gap). Rather than add a new managedBy enum value (which falls through to fully-editable platform defaults on deployed Console clients), the write policy is now the resolved affordance (resolveCrudAffordances = bucket default + userActions), and "engine-owned" is defined as a system/append-only object granting no write: - Writable set declares userActions{create,edit,delete}: the RBAC link tables, sys_user_preference, sys_approval_delegation, and the messaging config grids (notification preference/subscription/template). Affordance only — the DelegatedAdminGate / RLS / permission sets remain the authz. - Engine-owned objects locked to apiMethods ['get','list'] where absent (jobs, notifications, approvals, record-share, automation-run, mail/settings/secret audit, messaging delivery pipeline). sys_secret is read-locked explicitly (an empty apiMethods array fails open). - sys_import_job stays engine-owned: the REST import route writes job rows isSystem-elevated (attribution preserved via explicit created_by). - New engine write guard (assertEngineOwnedWriteAllowed, plugin-security) fail-closed rejects user-context generic writes keyed off resolved affordances; isSystem/context-less writes bypass. Wired into the security middleware alongside the other data-layer gates. - reconcileManagedApiMethods (objectql registry) now runs for every managed bucket, stripping advertised write verbs the affordances forbid. - /me/permissions clamp (plugin-hono-server) now clamps system/append-only too. ADR-0103. Refs #3220, follows #3222 / ADR-0049 / ADR-0092. Claude-Session: https://claude.ai/code/session_01Fp9yZxRQ3mb7p4vVwqFXKE Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses the concrete, safe slice of #3220 (the follow-through on #1591/#3213). A blast-radius audit determined the broad engine-level write guard over
system/append-onlyis not worth building — near-zero real security gain, high regression risk to delegated administration — so this PR ships only the two genuine latent holes the audit confirmed.What & why
Two non-better-auth managed objects shipped the exact contradiction #3213 fixed for better-auth objects:
enable.apiMethodsadvertised genericcreate/update/deletewhile theirmanagedBybucket forbids user-context writes — leaving the generic/dataroute open for a write the bucket does not permit.sys_presence(managedBy: 'append-only') advertisedcreate/update/delete—update/deleteon an append-only object — but is written only over the realtime websocket/in-memory path, never through ObjectQL. →['get', 'list'].sys_metadata(managedBy: 'system') advertised full CRUD, but customization overlays are authored only through the metadata-protocol RPC (its engine writes carry a transaction context, not a user session). Confirmed neither the framework nor the Console (objectui) POSTs/data/sys_metadata. →['get', 'list'].Reads stay open. The metadata-protocol / realtime write paths are engine-level and bypass the HTTP exposure gate, so they are unaffected.
Why not the broader guard
The audit found the
systembucket is overloaded: severalsystemobjects are user-writable by design —sys_user_position,sys_user_permission_set,sys_position_permission_set(the wholeDelegatedAdminGateis built on non-isSystemuser writes), plussys_user_preferenceandsys_import_job. A blanket affordance-driven engine guard would break delegated administration for near-zero gain (the genuinely engine-only tables already write viaisSystem, and the key audit tables —sys_audit_log/sys_activity— are already['get','list']). The real fix is a bucket-taxonomy split, tracked as the root cause in #3220.Verification
service-realtimesys-presence object suite (10 passed) — new assertion pinsapiMethods === ['get','list']and rejects re-introducing a write verb.metadata-core(100 passed);objectqloverlay engine-insert tests (6 passed) — confirmsys_metadataengine writes still work (they bypass the HTTP gate).package-first-authoring(5 passed) — end-to-end customization authoring unaffected.Refs #3220. Follows #3213 / ADR-0049 / ADR-0092.
🤖 Generated with Claude Code
Generated by Claude Code